﻿using System;
using System.IO;
using Fadd.JSON.Tokens;
using Array=Fadd.JSON.Tokens.Array;
using Boolean=Fadd.JSON.Tokens.Boolean;
using Object=Fadd.JSON.Tokens.Object;
using String=Fadd.JSON.Tokens.String;

namespace Fadd.JSON
{
	/// <summary>
	/// Reads a JSON document
	/// </summary>
	public class JsonReader
	{
		private readonly TextReader _reader;

		/// <summary>
		/// Initializes a new instance of the <see cref="JsonReader"/> class.
		/// </summary>
		/// <param name="stream">Stream to read from.</param>
		public JsonReader(Stream stream)
		{
			_reader = new StreamReader(stream);
			SkipWhiteSpaces();
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="JsonReader"/> class.
		/// </summary>
		/// <param name="reader">Stream to read from.</param>
		public JsonReader(TextReader reader)
		{
			_reader = reader;
			SkipWhiteSpaces();
		}
		/// <summary>
		/// Gets current token
		/// </summary>
		/// <exception cref="InvalidOperationException">Expected to find a token.</exception>
		/// <exception cref="System.ObjectDisposedException">Stream is disposed.</exception>
		public TokenId TokenId
		{
			get
			{
				SkipWhiteSpaces();
				char ch = Peek();
				switch (ch)
				{
					case 't':
					case 'f':
						return TokenId.Boolean;
					case '{':
						return TokenId.Object;
					case '}':
						return TokenId.EndOfObject;
					case ']':
						return TokenId.EndOfArray;
					case '[':
						return TokenId.Array;
					case '"':
						return TokenId.String;
					default:
						if (char.IsDigit(ch))
							return TokenId.Number;
						throw new InvalidOperationException("Expected to find a token.");
				}
			}
		}

		/// <summary>
		/// Currently read token
		/// </summary>
		/// <value>
		/// <c>null</c> if no node have been read.
		/// </value>
		public IToken Token { get; private set; }

		/// <summary>
		/// Read next token.
		/// </summary>
		/// <returns>true if successful; otherwise false.</returns>
		/// <exception cref="InvalidOperationException">JSON is malformed.</exception>
		/// <exception cref="System.ObjectDisposedException">Stream is disposed.</exception>
		public bool Read()
		{
			Token = ReadToken();
			return Token != null;
		}

		/// <summary>
		/// Read a token
		/// </summary>
		/// <returns>Token if found; otherwise null.</returns>
		/// <exception cref="InvalidOperationException">JSON is malformed.</exception>
		/// <exception cref="System.ObjectDisposedException">Stream is disposed.</exception>
		public IToken ReadToken()
		{
			switch (TokenId)
			{
				case TokenId.Array:
					return ReadArray();
				case TokenId.Number:
					return ReadNumber();
				case TokenId.String:
					return ReadString();
				case TokenId.Boolean:
					return ReadBoolean();
				case TokenId.Object:
					return ReadObject();
			}
			return null;
		}

		/// <summary>
		/// Read an object
		/// </summary>
		/// <returns>object if successful; null if next token is not an object.</returns>
		/// <exception cref="InvalidOperationException">JSON is malformed.</exception>
		/// <exception cref="System.ObjectDisposedException">Stream is disposed.</exception>
		public Object ReadObject()
		{
			if (Peek() != '{')
				return null;

			ReadChar();

			var item = new Object(string.Empty);
			char ch = Peek();
			while (_reader.Peek() >= 0)
			{
				switch (ch)
				{
					case '"':
						String name = ReadString();
						SkipWhiteSpaces();
						if (ReadChar() != ':')
							throw new InvalidOperationException("Expected colon after property name.");
						IToken value = ReadToken();
						value.Name = name.ToString();
						try
						{
							item.Add(value);
						}
						catch(ArgumentException err)
						{
							throw new InvalidOperationException("Object already contains a child with the name '" + value.Name + "'", err);
						}
						break;
					case ',':
						ReadChar();
						break;
					case '}':
						ReadChar();
						return item;
				}

				ch = Peek();
			}

			return item;
		}

		private Boolean ReadBoolean()
		{
			char[] tmp;
			switch (Peek())
			{
				case 't':
					tmp = new char[4];
					_reader.Read(tmp, 0, 4);
					if (tmp[0] == 't' && tmp[1] == 'r' && tmp[2] == 'u' && tmp[3] == 'e')
						return new Boolean(string.Empty, true);
					break;
				case 'f':
					tmp = new char[5];
					_reader.Read(tmp, 0, 5);
					if (tmp[0] == 'f' && tmp[1] == 'a' && tmp[2] == 'l' && tmp[3] == 's' && tmp[4] == 'e')
						return new Boolean(string.Empty, false);
					break;
			}

			return null;
		}

		private String ReadString()
		{
			if (Peek() != '"')
				return null;
			ReadChar();

			string value = string.Empty;
			char ch = ReadChar();
			char lastCh = char.MinValue;
			while (_reader.Peek() >= 0)
			{
				if (ch == '"' && lastCh != '\\')
					return new String(value);

				if (ch != '\\')
					value += ch;

				lastCh = ch;
				ch = ReadChar();
			}

			return new String(value);
		}

		private char ReadChar()
		{
			return (char) _reader.Read();
		}

		/// <summary>
		/// Read an array
		/// </summary>
		/// <returns>Array if successful; null if current token is not an array.</returns>
		/// <exception cref="InvalidOperationException">JSON is malformed.</exception>
		/// <exception cref="System.ObjectDisposedException">Stream is disposed.</exception>
		public Array ReadArray()
		{
			if (Peek() != '[')
				return null;

			ReadChar();
			var array = new Array();

			// Empty array.
			char ch = Peek();
			if (ch == ']')
			{
				ReadChar();
				return array;
			}

			while (_reader.Peek() >= 0)
			{
				IToken token = ReadToken();
				if (token == null)
					throw new InvalidOperationException("Expected to find a JSON token in array.");

				array.Add(token);

				SkipWhiteSpaces();
				ch = Peek();
				if (ch == ']')
				{
					ReadChar();
					return array;
				}

				if (Peek() != ',')
					throw new InvalidOperationException("Expected to find comma between values in array");

				ReadChar(); // read comma.
			}

			throw new InvalidOperationException("Missing array close token before end of stream.");
		}

		/// <summary>
		/// Read a number.
		/// </summary>
		/// <returns></returns>
		/// <exception cref="System.ObjectDisposedException">Stream is disposed.</exception>
		public Number ReadNumber()
		{
			if (!char.IsDigit(Peek()))
				return null;

			string number = string.Empty;
			char ch = Peek();
			while (_reader.Peek() >= 0 && (char.IsDigit(ch) || ch == '.'))
			{
				number += ch;
				ReadChar();
				ch = Peek();
			}

			return new Number(number);
		}


		/// <summary>
		/// Ignore white spaces and find next char.
		/// </summary>
		/// <returns>Next character, or <see cref="char.MinValue"/> if EOF.</returns>
		/// <exception cref="System.ObjectDisposedException">Stream is disposed.</exception>
		public char Peek()
		{
			if (_reader.Peek() == -1)
				return char.MinValue;

			SkipWhiteSpaces();
			try
			{
				return (char)_reader.Peek();
			}
			catch(IOException)
			{
				return char.MinValue;
			}
		}

		private void SkipWhiteSpaces()
		{
			while (_reader.Peek() >= 0 && char.IsWhiteSpace((char) _reader.Peek()))
				_reader.Read();
		}
	}
}